package com.katesoft.scale4j.persistent.spring.postprocessor;

import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.model.InternalDomainEntity;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.metadata.ClassMetadata;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean;

import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static com.katesoft.scale4j.common.services.IBeanNameReferences.SESSION_FACTORY;

/**
 * this post processor will store domain classes information into dedicated table's meta data table.
 * <p/>
 * this information can be useful for some plsql function(they can detect if schema's table is tracked by application or not).
 * <p/>
 * NOTE: this post processor can be disabled(if at least defined in spring context) by setting enabled flag to false(enabled by default).
 *
 * @author kate2007
 */
public class InternalDomainPostProcessor implements BeanPostProcessor
{
    private final Logger logger = LogFactory.getLogger(getClass());
    private boolean enabled = true;

    @Override
    public Object postProcessBeforeInitialization(Object bean,
                                                  String beanName) throws BeansException
    {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(final Object bean,
                                                 String beanName) throws BeansException
    {
        if (enabled) {
            if (AnnotationSessionFactoryBean.class.isAssignableFrom(bean.getClass()) && SESSION_FACTORY.equals(beanName)) {
                AnnotationSessionFactoryBean annotationSessionFactoryBean = (AnnotationSessionFactoryBean) bean;
                SessionFactory sessionFactory = annotationSessionFactoryBean.getObject();
                final Configuration configuration = annotationSessionFactoryBean.getConfiguration();
                Map<String, ClassMetadata> metadata = sessionFactory.getAllClassMetadata();
                final Iterator<Entry<String, ClassMetadata>> iterator = metadata.entrySet().iterator();
                HibernateTemplate template = new HibernateTemplate(sessionFactory);
                template.execute(new HibernateCallback<Object>()
                {
                    @SuppressWarnings({"unchecked"})
                    @Override
                    public Object doInHibernate(Session session) throws HibernateException, SQLException
                    {
                        Transaction transaction = session.beginTransaction();
                        try {
                            List<InternalDomainEntity> existingEntities = session.createCriteria(InternalDomainEntity.class).list();
                            List<InternalDomainEntity> newEntities = new LinkedList<InternalDomainEntity>();
                            while (iterator.hasNext()) {
                                Entry<String, ClassMetadata> entry = iterator.next();
                                PersistentClass persistentClass = configuration.getClassMapping(entry.getKey());
                                InternalDomainEntity internalDomainEntity = new InternalDomainEntity();
                                internalDomainEntity.setEntityName(entry.getKey());
                                internalDomainEntity.setEntityClass(persistentClass.getClassName());
                                internalDomainEntity.setTableName(persistentClass.getTable().getName());
                                InternalDomainEntity existingDefinition = findExistingDefinition(existingEntities, internalDomainEntity);
                                if (existingDefinition == null) {
                                    session.save(internalDomainEntity);
                                    logger.debug("saving %s", ReflectionToStringBuilder.toString(internalDomainEntity));
                                }
                                else {
                                    if (!EqualsBuilder.reflectionEquals(existingDefinition, internalDomainEntity)) {
                                        existingDefinition.setEntityClass(internalDomainEntity.getEntityClass());
                                        existingDefinition.setEntityName(internalDomainEntity.getEntityName());
                                        existingDefinition.setTableName(internalDomainEntity.getTableName());
                                        logger.debug("updating %s", ReflectionToStringBuilder.toString(existingDefinition));
                                        session.update(existingDefinition);
                                    }
                                }
                                newEntities.add(internalDomainEntity);
                            }
                            session.flush();
                            Collection<InternalDomainEntity> orphanEntities = findOrphanEntities(existingEntities, newEntities);
                            if (!orphanEntities.isEmpty()) {logger.debug("removing orphan internal domain entities %s", orphanEntities);}
                            for (InternalDomainEntity orphanEntity : orphanEntities) {
                                session.delete(orphanEntity);
                            }
                            session.flush();
                            transaction.commit();
                        }
                        catch (Exception e) {
                            transaction.rollback();
                            logger.error(e);
                        }
                        return null;
                    }

                    private Collection<InternalDomainEntity> findOrphanEntities(List<InternalDomainEntity> existingCollection,
                                                                                List<InternalDomainEntity> newCollection)
                    {
                        Collection<InternalDomainEntity> orphan = new LinkedHashSet<InternalDomainEntity>();
                        for (InternalDomainEntity internalDomainEntity : existingCollection) {
                            if (findExistingDefinition(newCollection, internalDomainEntity) == null) {
                                orphan.add(internalDomainEntity);
                            }
                        }
                        return orphan;
                    }

                    private InternalDomainEntity findExistingDefinition(List<InternalDomainEntity> entities,
                                                                        InternalDomainEntity actual)
                    {
                        for (InternalDomainEntity entity : entities) {
                            if (entity.getEntityName().equalsIgnoreCase(actual.getEntityName())) {
                                return entity;
                            }
                        }
                        return null;
                    }
                });
            }
        }
        return bean;
    }

    public void setEnabled(boolean enabled)
    {
        this.enabled = enabled;
    }
}

